import os
import time
from datetime import datetime, timedelta
import json
from hashlib import md5

class Session:
    def __init__(self, store, session_id=None):
        self.store = store
        self.session_id = session_id
        self.accessed = False
        self.dirty = False
        self.is_new = False

    def __contains__(self, key):
        return key in self.data

    def __getitem__(self, key):
        return self.data[key]

    def __setitem__(self, key, value):
        self.data[key] = value
        self.dirty = True

    def __delitem__(self, key):
        del self.data[key]
        self.dirty = True

    def keys(self):
        return self.data.keys()

    def values(self):
        return self.data.values()

    def items(self):
        return self.data.items()

    def get(self, key, default=None):
        return self.data.get(key, default)

    def pop(self, key, *args):
        self.dirty = self.dirty or key in self.data
        return self.data.pop(key, *args)

    def setdefault(self, key, value):
        if key in self.data:
            return self.data[key]
        else:
            self.dirty = True
            self.data[key] = value
            return value

    def update(self, dict_):
        self.data.update(dict_)
        if dict_:
            self.dirty = True

    def clear(self):
        if self.data:
            self.data.clear()
            self.dirty = True

    @property
    def data(self):
        """
        Lazily loads session from storage and cache it.
        """
        self.accessed = True
        try:
            return self._data
        except AttributeError:
            data = None
            if self.session_id is not None:
                data = self.store.load(self.session_id)
            if data is None:
                self.session_id = self.store.create()
                self.is_new = True
                data = {}
            self._data = data
            return data

    def flush(self):
        """
        Removes the current session data from the database and regenerates the id.
        session.flush usually should be called in logout.
        """
        if self.session_id:
            self.store.delete(self.session_id)
        self._data = {}
        self.session_id = self.store.create()
        self.accessed = True
        self.is_new = True
        self.dirty = False

    def cycle_session_id(self):
        """
        Creates a new session id, whilst retaining the current session data.
        """
        if self.session_id:
            id = self.session_id
            self.session_id = self.store.create(self.data)
            self.store.delete(id)
        else:
            self._data = {}
            self.session_id = self.store.create()
        self.accessed = True
        self.is_new = True
        self.dirty = False

    def save(self):
        if self.dirty:
            self.store.save(self.session_id, self.data)
            self.dirty = False

    def renew(self):
        self.store.renew(self.session_id)

class SessionStoreBase:
    def __init__(self, session_age=86400*10):
        self.session_age = session_age

    def get_new_session_id(self):
        "Returns session id that isn't being used."
        return md5(
                os.urandom(8) 
                + (str(os.getpid()) + str(time.time())).encode('latin-1')
            ).hexdigest()


    def get_expiry_date(self):
        """Get session the expiry date (as a datetime object)."""
        return datetime.utcnow() + timedelta(seconds=self.session_age)

    def encode(self, session_dict):
        if session_dict:
            return json.dumps(session_dict)
        return ''

    def decode(self, s):
        if not s:
            return {}
        try:
            return json.loads(s)
        except ValueError:
            return None

    def create(self, data=None):
        """
        Creates a new session instance with a unique id in storage.
        Returns session id of newly created session.
        """
        raise NotImplementedError()

    def save(self, session_id, data):
        raise NotImplementedError()

    def renew(self):
        raise NotImplementedError()

    def delete(self, session_id):
        """
        Deletes the session data under this id.
        """
        raise NotImplementedError()

    def load(self, session_id):
        """
        Loads the session data and returns a dictionary or None if session_id not found.
        """
        raise NotImplementedError()

